/*******************************************************************************
 * Copyright 2011 Mauro Luigi Drago (drago.luigi@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.bytenose.extrautils.patterns.dispatcher;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import com.bytenose.extrautils.annotations.NonNullable;
import com.bytenose.extrautils.annotations.UtilityClass;
import com.bytenose.extrautils.collect.Iterables2;
import com.bytenose.extrautils.functional.MutableTuple2;

import com.google.common.base.*;
import com.google.common.collect.*;

/**
 * A utility class defining filters for methods
 * based on the parameters.
 * @author Mauro Luigi Drago
 *
 */
@UtilityClass
public class ParamFilters {
	
	/**
	 * Private constructor to avoid instantiation.
	 */
	private ParamFilters() {
	}
	
	/**
	 * A filter matching methods with a specified number of 
	 * parameters.
	 * @author Mauro Luigi Drago
	 *
	 */
	private static class ParamsNumberFilter implements Predicate<Method> {
		/** The required number of parameter */
		private final int paramsNumber;
		
		/** The type of comparison to perform */
		private final ComparisonOp cop;
		
		/**
		 * Creates a new {@link ParamsNumberFilter}.
		 * @param paramsNumber
		 * 		the required number of parameters, cannot be negative.
		 * @param cop
		 * 		the comparison operator to select methods according to
		 * 		their number of arguments, cannot be null.
		 * @throws IllegalArgumentException
		 * 		if {@code paramsNumber} is negative or some of the
		 * 		{@link NonNullable} arguments are null.
		 */
		private ParamsNumberFilter(int paramsNumber, ComparisonOp cop) 
			throws IllegalArgumentException {
			if (paramsNumber < 0) throw new IllegalArgumentException("paramsNumber");
			if (cop == null) throw new IllegalArgumentException("cop");
			
			this.paramsNumber = paramsNumber;
			this.cop = cop;
		}
		
		@Override
		public boolean apply(Method m) {
			return cop.eval(m.getParameterTypes().length, paramsNumber);
		}
	}
	
	/**
	 * Creates a new filter matching methods with a specified number
	 * of parameters.
	 * @param paramsNumber
	 * 		the required number of parameters, cannot be negative.
	 * @return the filter.
	 * @throws IllegalArgumentException
	 * 		if {@code paramsNumber} is negative.
	 */
	@NonNullable
	public static Predicate<Method> getParamsNumberFilter(int paramsNumber) 
			throws IllegalArgumentException {
		return new ParamsNumberFilter(paramsNumber, ComparisonOp.EQ);
	}
	
	/**
	 * Creates a new filter matching methods according to their number
	 * of parameters. 
	 * The number of arguments for the method will be the left operand 
	 * and {@code paramsNumber} the right operand for the comparison.
	 * @param paramsNumber
	 * 		the required number of parameters, cannot be negative.
	 * @return the filter.
	 * @param cop
	 * 		the comparison operator to select methods according to
	 * 		their number of arguments, cannot be null.
	 * @throws IllegalArgumentException
	 * 		if {@code paramsNumber} is negative or some of the
	 * 		{@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static Predicate<Method> getParamsNumberFilter(
			int paramsNumber, @NonNullable ComparisonOp cop) 
			throws IllegalArgumentException {
		return new ParamsNumberFilter(paramsNumber, cop);
	}
	
	/**
	 * A filter matching methods which do not have a return type.
	 * @author Mauro Luigi Drago
	 *
	 */
	private static class VoidReturnFilter implements Predicate<Method> {
		/**
		 * Creates a new {@link VoidReturnFilter}.
		 */
		private VoidReturnFilter() {
		}
		
		@Override
		public boolean apply(Method m) {
			return m.getReturnType().equals(Void.class);
		}
	}
	
	/**
	 * Creates a new filter matching methods which do 
	 * not have a return value. 
	 */
	@NonNullable
	public static Predicate<Method> getVoidReturnFilter() {
		return new VoidReturnFilter();
	}
	
	/**
	 * Creates a new filter matching methods which 
	 * have a return value.
	 * @return the filter. 
	 */
	@NonNullable
	public static Predicate<Method> getNonVoidReturnFilter() {
		return Predicates.not(new VoidReturnFilter());
	}
	
	/**
	 * A filter matching methods which have a return type of a specified
	 * type. 
	 * @author Mauro Luigi Drago
	 *
	 */
	private static class ReturnTypeFilter implements Predicate<Method> {
		/** The type of the return parameter */
		private final Class<?> returnType;
		
		/** A flag indicating if the method return type should match exactly
		 *  the specified type (true) or should be compatible (false).
		 */
		private final boolean exactMatch;
		
		/**
		 * Creates a new {@link ReturnTypeFilter}.
		 * @param returnType
		 * 		the required type of the return parameter, cannot be null.
		 * @param exactMatch
		 * 		A flag indicating if the method return type should match exactly
		 *  	the specified type (true) or should be compatible (false).
		 * @throws IllegalArgumentException
		 * 		if some of the {@link NonNullable} arguments are null.
		 */
		private ReturnTypeFilter(@NonNullable Class<?> returnType,
				boolean exactMatch) 
			throws IllegalArgumentException {
			if (returnType == null) throw new IllegalArgumentException("returnType");
			
			this.returnType = returnType;
			this.exactMatch = exactMatch;
		}
		
		@Override
		public boolean apply(Method m) {
			Class<?> mrt = m.getReturnType();
			if (mrt == null) return false;
			if (exactMatch) return returnType.equals(mrt);
			else return returnType.isAssignableFrom(mrt);
		}
	}
	
	/**
	 * Creates a new filter matching methods which have a return 
	 * type of a specified type. 
	 * The return type of the method must be equal to the
	 * specified type.
	 * @param returnType
	 * 		the required type of the return parameter, cannot be null.
	 * @return the filter.
	 * @throws IllegalArgumentException
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static Predicate<Method> getReturnTypeFilter(
			@NonNullable Class<?> returnType) 
			throws IllegalArgumentException {
		return Predicates.and(
				getNonVoidReturnFilter(),
				new ReturnTypeFilter(returnType, true));
	}
	
	/**
	 * Creates a new filter matching methods which have a return 
	 * type of a specified type. If the {@code exactMatch} parameter
	 * is true, the return type of the method must be equal to the
	 * specified type, otherwise only compatibility is requested (i.e.,
	 * the return type of the method is assignable to the specified type). 
	 * @param returnType
	 * 		the required type of the return parameter, cannot be null.
	 * @param exactMatch
	 * 		A flag indicating if the method return type should match exactly
	 *  	the specified type (true) or should be compatible (false).
	 * @return the filter.
	 * @throws IllegalArgumentException
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static Predicate<Method> getReturnTypeFilter(
			@NonNullable Class<?> returnType, boolean exactMatch) 
			throws IllegalArgumentException {
		return Predicates.and(
				getNonVoidReturnFilter(),
				new ReturnTypeFilter(returnType, exactMatch));
	}
	
	/**
	 * A filter matching methods which have parameters of the specified type. 
	 * @author Mauro Luigi Drago
	 *
	 */
	private static class ParamsTypeFilter implements Predicate<Method> {
		/** The type of the method parameters */
		private final List<Class<?>> paramsTypes;
		
		/** A flag indicating if the arguments types type should match 
		 *  exactly (true) or should be compatible (false).
		 */
		private final boolean exactMatch;
		
		/**
		 * Creates a new {@link ReturnTypeFilter}.
		 * @param paramsTypes
		 * 		the required types of the method parameters, cannot be null.
		 * @param exactMatch
		 * 		A flag indicating if the method return type should match exactly
		 *  	the specified type (true) or should be compatible (false).
		 * @throws IllegalArgumentException
		 * 		if some of the {@link NonNullable} arguments are null.
		 */
		private ParamsTypeFilter(
				@NonNullable List<Class<?>> paramsTypes,
				boolean exactMatch) 
			throws IllegalArgumentException {
			if (paramsTypes == null) throw new IllegalArgumentException("paramsTypes");
			
			this.paramsTypes = paramsTypes;
			this.exactMatch = exactMatch;
		}
		
		@Override
		public boolean apply(Method m) {
			return Iterables.all(
					Iterables2.zip(
							Arrays.asList(m.getParameterTypes()),
							paramsTypes),
					new Predicate<MutableTuple2<Class<?>,Class<?>>>(){
						@Override
						public boolean apply(MutableTuple2<Class<?>, Class<?>> tuple) {
							// Value 1 is the class of the method argument
							// Value 2 is the required class of the method argument
							// Value 2 must be compatible with Value 1 (equal or more specific)
							if (exactMatch) return tuple.getValue1().equals(tuple.getValue2());
							else return tuple.getValue1().isAssignableFrom(tuple.getValue2());
						}
					});
		}
	}
	
	/**
	 * Creates a new filter matching methods which 
	 * which have parameters of the specified type.
	 * Arguments and required types must be equal. 
	 * @param paramsTypes
	 * 		the required types of the method parameters, cannot be null.
	 * @return the filter.
	 * @throws IllegalArgumentException
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static Predicate<Method> getParamsTypeFilter(
			@NonNullable List<Class<?>> paramsTypes) {
		if (paramsTypes == null) throw new IllegalArgumentException("paramsTypes");
		return Predicates.and(
				getParamsNumberFilter(paramsTypes.size()),
				new ParamsTypeFilter(paramsTypes, true)
				);
	}
	
	/**
	 * Creates a new filter matching methods which 
	 * which have parameters of the specified type. 
	 * @param paramsTypes
	 * 		the required types of the method parameters, cannot be null.
	 * @param exactMatch
	 * 		A flag indicating if the method return type should match exactly
	 *  	the specified type (true) or should be compatible (false).
	 * @return the filter.
	 * @throws IllegalArgumentException
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static Predicate<Method> getParamsTypeFilter(
			@NonNullable List<Class<?>> paramsTypes,
			boolean exactMatch) {
		if (paramsTypes == null) throw new IllegalArgumentException("paramsTypes");
		return Predicates.and(
				getParamsNumberFilter(paramsTypes.size()),
				new ParamsTypeFilter(paramsTypes, exactMatch)
				);
	}
}
